
#include <hk32f030m.h>
#include <hk32f030m_gpio.h>
#include <hk32f030m_rcc.h>

#include <adc/cm_hk030.hpp>
#include <ioxx_button_trigger.hpp>
#include <lipid_filter.hpp>
#include <pin/cm_hk030.hpp>
#include <scheduler_basic.hpp>
#include <systick_counter.hpp>


/* ================================== 定时任务配置 ======================================= */

using TimeSource = scheduler_basic::SysTickMsSource;
using TimeType = TimeSource::TimeType;

// 初始化定时调度器对象，最大任务数10
namespace sb = scheduler_basic;
sb::DelayCallback3<TimeSource, 10> dcall;


/* ================================== 数码管引脚定义 ======================================= */

constexpr size_t DIG_COUNT = 5;
constexpr size_t SEG_COUNT = 8;

// 数码管对应8 + 5 一共13 个引脚，全都是推挽输出模式，
// 用union 把seg 引脚和dig 引脚的数组连到一个长度13 的数组all_pin 里
// 方便批量设置GPIO 模式
const union {
    ioxx::Pin all_pin[SEG_COUNT + DIG_COUNT];

    struct {
        ioxx::Pin seg[SEG_COUNT];
        ioxx::Pin dig[DIG_COUNT];
    } pin;

} TUBE = {
    .pin = {

        // ======== 共阴数码管8 段引脚 ==========
        // 用一个数组把八个段引脚装一起方便处理
        // 8 段对应一个字节，按顺序，A 是LSB，DP 是MSB
        // 所以SEG_A 在数组里是是[0]，
        .seg = {
            ioxx::Pin(GPIOD, 1),  // 数码管SEG A 引脚，连接到阳极
            ioxx::Pin(GPIOC, 7),  // SEG B
            ioxx::Pin(GPIOC, 6),  // SEG C
            ioxx::Pin(GPIOC, 5),  // SEG D
            ioxx::Pin(GPIOC, 4),  // SEG E
            ioxx::Pin(GPIOC, 3),  // SEG F
            ioxx::Pin(GPIOB, 4),  // SEG G
            ioxx::Pin(GPIOD, 6),  // 小数点 DP
        },

        // ======= 共阴数码管5 位COM 引脚 =============
        // 连接到数码管阴极，所以低电平有效
        // 同样装在一个数组里，[0] 对应左侧数码管第一位
        // DIG4 同时作为按键输入引脚，要设置为开漏输出，且启用内部下拉，以读取按键输入的高电平信号
        .dig = {
            ioxx::Pin(GPIOA, 3),  // DIG3
            ioxx::Pin(GPIOD, 4),  // DIG4

            ioxx::Pin(GPIOA, 0),  // DIG0
            ioxx::Pin(GPIOA, 1),  // DIG1
            ioxx::Pin(GPIOA, 2),  // DIG2
        }}};


constexpr size_t DIG4_KEY_POS = 1;  // DIG4 在dig 数组中的位置

/* =============================================== 数码管引脚操作 ============================================ */


/** 初始化所有数码管引脚为输出模式，并将位选引脚设为高电平，关闭数码管显示
 *
 * 引脚直接驱动数码管需要比较大的电流，驱动速度应该选比较高的等级
 */
void init_tube_pin() {
    using namespace ioxx;
    PinInit pi;
    pi.mode(mode::out).drive(drive::open_drain).pull(pull::down);

    // 设置所有引脚为开漏输出，启用内部下拉
    for (const Pin& p : TUBE.all_pin) {
        pi.init(p);

        // 拉高所有引脚，默认关闭数码管
        p.set();
    }
}


/** 用一个字节按顺序设置数组内引脚的输出电平，数组[0] 对应LSB
 */
void write_pins_by_byte(const ioxx::Pin* pins, size_t pins_count, uint8_t b) {
    for (; pins_count > 0; --pins_count) {
        if (0x01 & b) {
            pins->set();
        }
        else {
            pins->clr();
        }

        b = b >> 1;
        ++pins;
    }
}


/**
 * @brief 写入段选引脚，SEG A 对应[0]
 *
 * @param b
 */
void write_seg_by_byte(uint8_t b) {
    write_pins_by_byte(TUBE.pin.seg, SEG_COUNT, b);
}


void nrst_switch_to_pa0() {
    // 切换NRST 为PA0
    RCC->APB1ENR |= RCC_APB1ENR_IOMUXEN;
    GPIOMUX->NRST_PIN_KEY = 0x5AE1;
    GPIOMUX->NRST_PA0_SEL = 1;
}


/* ================================== 按键处理 ======================================= */


// 实现按键消抖和点击、长按逻辑
static ioxx::ButtonTrigger<ioxx::polarity::high> button_trigger;


/* ================================== 数码管显示操作 ======================================= */

// 共阴数码管码表：
//                                       0      1     2    3     4      5      6     7     8    9
constexpr static uint8_t TUBE_CODE[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

constexpr uint8_t TUBE_CODE_DP_MASK = 0x80;  // 添加小数点的bit MASK
constexpr uint8_t TUBE_CODE_NONE = 0x00;     // 无显示
constexpr uint8_t TUBE_CODE_MINUS = 0x40;    // 显示中间横杠，或负号
constexpr uint8_t TUBE_CODE_U = 0x3E;        // 显示字母U
constexpr uint8_t TUBE_CODE_J = 0x0E;        // 显示字母J
constexpr uint8_t TUBE_CODE_P = 0x73;
constexpr uint8_t TUBE_CODE_A = 0x77;
constexpr uint8_t TUBE_CODE_C = 0x39;
constexpr uint8_t TUBE_CODE_L = 0x38;


// 显示缓冲区
// 0, 1 对应左侧两位的数码管；2, 3, 4 对应右侧的三位
static uint8_t display_buffer[DIG_COUNT];

// 数码管单个位的刷新周期，如果周期是20ms，则每个位都以50Hz 频率闪烁
// 20ms 要平分给5 位数码管，所以每个位点亮4ms 然后就熄灭16ms，切换到其他位，所以位的切换周期是4ms，频率250Hz。
// 刷新周期太大会让数码管看起来有明显的闪烁，太小切换太频繁，又会占用过多CPU 时间，50Hz 一般应该没问题
constexpr TimeType DISPLAY_REFRESH_PERIOD = 20;

// 位选的切换周期，5 个位平分整个刷新周期，所以就是刷新周期除以5
constexpr TimeType DIG_SWITCH_PERIOD = DISPLAY_REFRESH_PERIOD / DIG_COUNT;

// 按键扫描周期大于等于7ms
constexpr TimeType KEY_SCAN_PERIOD = 9;


/** 定时刷新数码管的延时任务，同时负责定时读取按键
 *
 * 所有SEG 引脚经过数码管中的二极管和各个DIG 引脚连接，
 * 所以要从DIG4 读取按键输入，必须先将所有SEG 拉低，去除干扰
 *
 */
TimeType callback_task_cycle_display() {
    static size_t dig_index = 0;

    static bool is_time_to_key_scan = true;

    write_seg_by_byte(0x00);  // 先将SEG 全部拉低

    // 切换位选引脚，拉高上一个位选，之后再拉低当前位选
    TUBE.pin.dig[dig_index].set();

    ++dig_index;
    if (dig_index >= DIG_COUNT) {
        dig_index = 0;
    }

    // 到时间扫描按键
    if (is_time_to_key_scan) {
        is_time_to_key_scan = false;

        // 添加定时任务，到时间再通知扫描按键
        dcall.add_task(

            []() -> TimeType {
                is_time_to_key_scan = true;
                return 0;
            },

            KEY_SCAN_PERIOD);

        // 直接读取DIG4 的电平，因为此时所有DIG 引脚都是拉高的状态，
        // 不必判断当前是哪个DIG 引脚。开漏输出引脚拉高时可以直接读取输入电平
        button_trigger.feed(TUBE.pin.dig[DIG4_KEY_POS].get());
    }

    // 拉低当前位选
    TUBE.pin.dig[dig_index].clr();
    write_seg_by_byte(display_buffer[dig_index]);  // 写入显示值

    return DIG_SWITCH_PERIOD;
}


/* ================================== 显示数据操作 ======================================= */

enum class display_mode {
    u_j = 0,  // 左侧两位数码管显示U 表示电压，右侧三位显示J，表示电流
    u_p = 1,  // 两位显示电压U，三位显示功率P
    j_u = 2,  // 反过来，用三位显示电压，两位显示电流

    // TODO:
    cal = 3,  // 校准模式
};


constexpr int DISPLAY_MODE_NUM_MIN = 0;
constexpr int DISPLAY_MODE_NUM_MAX = 3;


display_mode current_display_mode = display_mode::u_j;


/**
 * @brief 存储4 个msg 对应的显示值的数组，内层数组顺序和display_buffer 相同
 */
constexpr static uint8_t MSG_DISPLAY_CODE[4][DIG_COUNT] = {
    {0X00, TUBE_CODE_U, 0x00, TUBE_CODE_J, 0X00},  // 0: U J
    {0X00, TUBE_CODE_U, 0X00, TUBE_CODE_P, 0X00},  // 1: U P
    {0X00, TUBE_CODE_J, 0X00, TUBE_CODE_U, 0X00},  // 2: J U

    //{0X00, 0X00, TUBE_CODE_C, TUBE_CODE_A, TUBE_CODE_L},  // 3: CAL
};


// is_messaging 为true 时，表示数码管正用于显示静态的状态信息，一段时间内禁止更新显示缓冲区
// 测量数据会被丢弃，不显示
static bool is_messaging = false;


/**
 * @brief 将选定的msg 送进显示缓冲区，进入messaging 状态，一段时间内不会更新显示数据
 *
 * 如果在messaging 状态再次发出新的msg，则更新显示，倒计时重置
 *
 * @param t
 */
void show_mode_message() {
    // 数码管显示状态信息的时间，超过时间后，重新开始显示测量数据
    constexpr TimeType MSG_DURATION = 1000;

    static int_fast8_t task_index;

    auto msg_code = MSG_DISPLAY_CODE[static_cast<int>(current_display_mode)];

    for (int_fast8_t i = 0; i < DIG_COUNT; ++i) {
        display_buffer[i] = msg_code[i];
    }

    if (!is_messaging) {
        // 如果当前不是messaging 状态，则进入messaging
        is_messaging = true;

        /** 添加延时回调，定时退出messaging 状态
         */
        task_index = dcall.add_task(

            []() -> TimeType {
                is_messaging = false;
                return 0;
            },

            MSG_DURATION);
    }
    else {
        // 如果已经是messaging 状态，则重置任务的倒计时，
        // 避免重复多次添加任务把调度挤满
        dcall.reset_task(task_index, MSG_DURATION);
    }
}


// 进入U_P 模式前的状态
static display_mode last_mode_before_u_p;

/**
 * @brief 在U_J 和J_U 模式中切换，然后调用show_mode_message
 *
 * 如果当前模式是U_P 则切换为进入U_P 模式之前的状态
 *
 */
void cycle_u_j_mode() {
    if (current_display_mode == display_mode::u_p) {
        current_display_mode = last_mode_before_u_p;
    }
    else if (current_display_mode == display_mode::j_u) {
        current_display_mode = display_mode::u_j;
    }
    else {
        current_display_mode = display_mode::j_u;
    }

    show_mode_message();
}


/**
 * @brief 切换为U_P 模式
 *
 */

void switch_to_u_p_mode() {
    if (current_display_mode != display_mode::u_p) {
        last_mode_before_u_p = current_display_mode;
        current_display_mode = display_mode::u_p;
    }

    show_mode_message();
}


/**
 * @brief 将mA ，mV ，mW数值转换成A ,V 或W 的显示数据，添加小数点
 *
 * 数值不能是负数，如果数值大于9V 或9A，则结果保留一位小数，否则保留两位小数。
 * 功率值有可能超过99W，没有小数部分
 * 计算得到三位数字结果后，再转换成数码管显示值，然后依次送进buf 指针指定显示缓冲区位置
 *
 * @param num
 * @param buf
 */
void milli_num_to_3_digit(uint32_t num, uint8_t* buf) {
    // 在messaging 状态不更新显示值
    if (is_messaging) {
        return;
    }

    uint8_t temp_buf[5] = {0};
    uint_fast8_t c;

    // 从十位数开始，将num 拆解成5 个显示值
    for (int_fast8_t i = 4; i >= 0; --i) {
        num /= 10;
        c = num % 10;
        temp_buf[i] = TUBE_CODE[c];
    }

    // 添加小数点
    temp_buf[2] |= TUBE_CODE_DP_MASK;

    // 找出不为0 的第一个数的位置, 带小数点的0 != 0
    uint8_t* start_pos = temp_buf;
    while (*start_pos == TUBE_CODE[0]) {
        ++start_pos;
    }

    for (int_fast8_t i = 0; i < 3; ++i) {
        buf[i] = start_pos[i];
    }
}


/**
 * @brief 将数值送入显示缓冲区
 *
 * @param num 单位是mV，最大不超过99V
 */
void show_milli_volt_amp_watt(uint32_t volt, uint32_t amp) {
    // 如果是U_J 或U_P 模式，电压在左侧，显示两位数
    // 所以milli_num_to_3_digit 从显示缓冲区[0] 开始写入3 个转换值
    // 写在[2] 处的多余的1 个数在之后显示J 或P 时覆盖掉
    uint32_t left = volt;
    if (current_display_mode == display_mode::j_u) {
        left = amp;
    }

    milli_num_to_3_digit(left, display_buffer);

    uint32_t right = amp;
    if (current_display_mode == display_mode::j_u) {
        right = volt;
    }
    else if (current_display_mode == display_mode::u_p) {
        right = volt * amp / 1000;
    }

    milli_num_to_3_digit(right, &display_buffer[2]);
}


/* ==================================== ADC 输入 ========================================= */

// 电压引脚，连接分压器
const ioxx::Pin V_SENSE_PIN{GPIOD, 2};
constexpr auto V_SENSE_CHANNEL = adxx::channel::ain4;

// 电流引脚，连接运放
const ioxx::Pin I_SENSE_PIN{GPIOD, 3};
constexpr auto I_SENSE_CHANNEL = adxx::channel::ain3;


static adxx::Adc adcc;

void init_adc_pin() {
    using namespace ioxx;
    PinInit()
        .pull(pull::none)
        .mode(mode::analog)
        .init(I_SENSE_PIN)
        .init(V_SENSE_PIN);
}


void init_adc() {
    // 不连续模式
    adcc.init(adxx::adc_mode::discontinuous);

    // 使能ADC 通道
    // 反向转换时，次序是： 0 电压检测 1 电流检测
    adcc.set_channel({I_SENSE_CHANNEL, V_SENSE_CHANNEL});

    // 设置ADC 系数固定为3.3V / 4095
    // 用内置参考电压测出来的系数偏小，不如简单的用外部3.3V 电源
    adcc.ad_factor_by_full_scale(static_cast<uint32_t>(3.3E6));
}


/* ==================================== 数据处理 ========================================= */

using FilterValueType = uint32_t;
using FilterType = lipid::MovingAverageFilter<FilterValueType, FilterValueType, 16>;

// 电压、电流滑动平均滤波器，窗口尺寸 == 16
static FilterType adc_filter[2];

FilterType& v_filter = adc_filter[0];
FilterType& i_filter = adc_filter[1];


// 经过滤波后的ADC 采样原始值
static FilterValueType adc_avg[2] = {0};

FilterValueType& v_avg = adc_avg[0];
FilterValueType& i_avg = adc_avg[1];


// 显示数据更新周期设为300ms，毕竟数字变得太快了眼睛看不过来
// 这期间持续采样并滑动平均滤波

// 定时获取ADC 结果并启动下一个通道的采样
TimeType callback_task_adc_update() {
    // 每10 毫秒查询一次ADC 结果，3 个通道一共30 毫秒，足以在300ms 刷新周期内全部采样8 次
    constexpr TimeType ADC_UPDATE_PERIOD = 10;

    auto ch_index = adcc.completed();
    if (ch_index >= 0) {
        // 已转换完成，将ADC 值送进滤波器，获取滤波结果
        // 送进滤波器的值是乘了系数的电压值，单位微伏，这样或许可以降低ADC 的量化误差，平均以后有一点插值的效果
        // 假如ADC 转换结果每一级代表是10V，结果1 == 10V，2 == 20V。当输入电压为15V 时，转换结果无法精确表达15 % 10 = 5V 的部分，
        // 可能会在1 和2 之间反复横跳，送进滤波器的序列可能是1 2 1 1 2 2 这种感觉，如果先乘10 再滤波，
        // 序列变成了10 20 10 10 20 20，一取平均结果是15，本来没法表达的5V 部分就蹦出来了。
        adc_avg[ch_index] = adc_filter[ch_index].feed(adcc.fetch_real());

        // 继续启动转换
        adcc.start();
    }

    return ADC_UPDATE_PERIOD;
}


// 将ADC 采样结果转换为mV 或mA 电压，然后显示
TimeType callback_task_display_data_update() {
    constexpr TimeType DISPLAY_DATA_UPDATE_PERIOD = 300;

    uint32_t milli_v = v_avg / 1000 * 10;
    uint32_t milli_i = i_avg / 1000;

    milli_v += 200;  // 电压测量值似乎比实际值总是低0.2V(200mV)，所以补偿一下
                     // TODO: 之后添加校准功能，根据输入的标准电压自动计算补偿量

    // 电压测量电路有10 倍衰减，导致输入信号比较小，比如从5V 到9V，ADC 输入变化只有0.4V，
    // 所以电压测量精度比电流精度更低

    show_milli_volt_amp_watt(milli_v, milli_i);

    return DISPLAY_DATA_UPDATE_PERIOD;
}


/* ============================================ M  I  A  N  👀👀🫵👊============================================ */

/** 初始化代码
 */
void hw_setup() {
    // "在使用进入 stop 低功耗模式时，一定要记得将 PWR 时钟打开"
    // "没有打开 PWR 时钟，则会功能异常，且不能再次烧录程序"
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);

    // 配置SysTick
    scheduler_basic::setup_systick();

    // 启动中断
    __enable_irq();

    // 配置GPIO
    ioxx::enable_gpio();
    nrst_switch_to_pa0();

    // 数码管引脚初始化
    init_tube_pin();

    // ADC 初始化
    init_adc_pin();
    init_adc();
}


void setup() {
    hw_setup();

    // 启动ADC
    adcc.start();

    // 添加不会终止的定时任务
    // 调度器最大容量10 绰绰有余
    dcall.add_task(callback_task_cycle_display, 0);        // 数码管刷新任务
    dcall.add_task(callback_task_adc_update, 0);           // ADC 定时采样、滤波任务
    dcall.add_task(callback_task_display_data_update, 0);  // 显示数据刷新任务

    // 上电后默认先显示消息，指示当前模式
    show_mode_message();
    // TODO: 或许可以存储用户选择的模式
}


int main(void) {
    setup();

    while (1) {
        dcall.tick();

        // ====== 处理按键 =======
        // 单击切换电压、电流显示位置
        if (button_trigger.clicked()) {
            cycle_u_j_mode();
        }

        // 长按显示功率
        if (button_trigger.long_pressed()) {
            switch_to_u_p_mode();
        }
    }
}


// 兼容C 的Handler 函数

extern "C" {
// 如果中断函数直接放在cpp 文件，由于C++ 编译器会修改函数名，就和中断向量表的命名不匹配了


__attribute__((used)) void SysTick_Handler() {
    // user_code_insert_to_systick_handler();
    scheduler_basic::systick_interrupt_counter_inc();
}


/**
 * @brief 显示错误信息
 *
 * @param msg_str    错误信息字符串，以\0 结尾
 */
void assert_failed_message(char *msg_str) {

}


/**
 * @brief  Reports the name of the source file and the source line number
 *         where the assert_param error has occurred.
 * @param  file: pointer to the source file name
 * @param  line: assert_param error line source number
 * @retval None
 */
void assert_failed(char* file, uint32_t line) {
    /* User can add his own implementation to report the file name and line number,
       tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
    /* Infinite loop */

    while (1) {
    }
}

}
